//============================================================================
// UsbLinkWorkerThread.cs
//
// Copyright (c) 2005-2006 U.S. TrailMaps, LLC
// THIS MATERIAL IS CONFIDENTIAL AND PROPRIETARY TO U.S. TRAILMAPS AND
// MAY NOT BE REPRODUCED, PUBLISHED OR DISCLOSED TO OTHERS WITHOUT COMPANY
// AUTHORIZATION.
//
// This work is derived from Waymex GPS Library for .Net version 2.7.9 source.
// Portions copyright (c) 2004-2005 Waymex IT Ltd
//============================================================================

// Turn this define on to emulate a USB GPS device. This is very useful for diagnosing a customer problem given a GPS trace log file.
// Set up the device responses to match the responses from the trace file in the UsbLinkWorkerThread.DoTransmit method.
//#define EMULATE_DEVICE

using System;
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;

namespace Waymex.Gps.Garmin.Usb
{
	/// <summary>
	/// The USB link worker thread.
	/// </summary>
	/// <remarks>
	/// This class contains both the worker thread code itself, along with methods that can be executed from the UI thread to manage
	/// and communicate with the worker thread. All communication and marshaling between threads is handled automatically.
	/// </remarks>
	internal class UsbLinkWorkerThread : IDisposable
	{
		private const int ASYNC_DATA_SIZE = 64;
		private const int MAX_BUFFER_SIZE = 4096;
        private static uint IOCTL_ASYNC_IN = NativeWin32Methods.CTL_CODE(NativeWin32Methods.FILE_DEVICE_UNKNOWN, 0x850, NativeWin32Methods.METHOD_BUFFERED, NativeWin32Methods.FILE_ANY_ACCESS);
		private const int pingTimeoutMilliseconds = 500;
		private const int transmitTimeoutMilliseconds = 5000;
		private const int receiveTimeoutMilliseconds = 5000;
        private const string ERR_9009_MSG = "Timed out during a Read Operation.";

		private IntPtr deviceHandle;
		private int usbPacketSize;
		private LinkPacketIds linkPacketIds;
		private Thread thread;
		private ManualResetEvent startTransmitEvent = new ManualResetEvent(false);
		private ManualResetEvent transmitCompleteEvent = new ManualResetEvent(false);
		private ManualResetEvent startReceiveEvent = new ManualResetEvent(false);
		private ManualResetEvent receiveCompleteEvent = new ManualResetEvent(false);
		private ManualResetEvent pingEvent = new ManualResetEvent(false);
		private ManualResetEvent pingResponseEvent = new ManualResetEvent(false);
		private ManualResetEvent exitThreadEvent = new ManualResetEvent(false);
		private Queue transmitPacketQueue = Queue.Synchronized(new Queue());
		private Queue receivePacketQueue = Queue.Synchronized(new Queue());
		private NativeBuffer interruptInBuffer = new NativeBuffer(ASYNC_DATA_SIZE);
		private NativeBuffer bulkInBuffer = new NativeBuffer(MAX_BUFFER_SIZE);
		private Exception workerThreadUnhandledException;

#if EMULATE_DEVICE
		private Queue interruptInEmulQueue = new Queue();
		private Queue bulkInEmulQueue = new Queue();

		private static byte[] BytesFromLogData(string logData)
		{
			if (logData.Length > 0)
			{
				string[] bytesData = logData.Split('-');
				byte[] bytes = new byte[bytesData.Length];
				for (int index = 0; index < bytesData.Length; index++)
					bytes[index] = byte.Parse(bytesData[index], NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
				return bytes;
			}
			else
				return null;
		}
#endif

		//
		// The following methods are meant to run from the UI thread.
		//

		public UsbLinkWorkerThread(IntPtr deviceHandle, int usbPacketSize, LinkPacketIds linkPacketIds)
		{
			this.deviceHandle = deviceHandle;
			this.usbPacketSize = usbPacketSize;
			this.linkPacketIds = linkPacketIds;
		}

		~UsbLinkWorkerThread()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		private bool disposed = false;

		private void Dispose(bool disposing)
		{
			if (!disposed)
			{
				Stop();
				interruptInBuffer.Dispose();
				bulkInBuffer.Dispose();
			}
		}

		private void CheckForWorkerThreadUnhandledException()
		{
			if (workerThreadUnhandledException != null)
			{
                //if (GpsTrace.GpsSwitch.TraceError)
                //    GpsTrace.WriteLine("ERROR: UsbLinkWorkerThread reported an unhandled exception: {0}", workerThreadUnhandledException.Message);
				Exception ex = workerThreadUnhandledException;
				workerThreadUnhandledException = null;
				throw ex;
			}
		}

		public void Start()
		{
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Starting worker thread");
			Debug.Assert(thread == null);
			startTransmitEvent.Reset();
			startReceiveEvent.Reset();
			pingEvent.Reset();
			exitThreadEvent.Reset();
			workerThreadUnhandledException = null;
			thread = new Thread(new ThreadStart(this.ThreadLoop));
			thread.Start();
		}

		public void Stop()
		{
			if (thread != null)
			{
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Stopping worker thread");
				exitThreadEvent.Set();
				if (!thread.Join(1000))
				{
                    //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceError, "ERROR: Thread did not exit; attempting abort");
					thread.Abort();
				}
				thread = null;
			}
		}

		/// <summary>
		/// Ensure that the worker thread is ready to accept commands.
		/// </summary>
		/// <remarks>
		/// It is possible for the worker thread to get "locked up" on an I/O API call when the conversation with the device
		/// doesn't proceed as it should. This method detects this situation and attempts to rectify it when found by
		/// aborting and restarting the thread.
		/// </remarks>
		private void MakeSureThreadIsResponsive()
		{
			CheckForWorkerThreadUnhandledException();
			pingResponseEvent.Reset();
			pingEvent.Set();
			if (!pingResponseEvent.WaitOne(pingTimeoutMilliseconds, false))
			{
				CheckForWorkerThreadUnhandledException();
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceError, "ERROR: Thread is not responsive; stopping and restarting");
				Stop();
				Start();
			}
		}

		public void TransmitPacket(UsbLinkPacket packet)
		{
			MakeSureThreadIsResponsive();
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Sending packet to worker thread to transmit");
			transmitPacketQueue.Enqueue(packet);
			transmitCompleteEvent.Reset();
			startTransmitEvent.Set();
			if (!transmitCompleteEvent.WaitOne(transmitTimeoutMilliseconds, false))
			{
				CheckForWorkerThreadUnhandledException();
				throw new UsbLinkException("Timed out transmitting a packet to the USB device.");
			}
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Worker thread completed transmit");
		}

		public void ClearReceivePacketQueue()
		{
			receivePacketQueue.Clear();
		}

		public UsbLinkPacket ReceivePacket()
		{
			if (receivePacketQueue.Count == 0)
			{
				MakeSureThreadIsResponsive();
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Triggering worker thread to receive data");
				receiveCompleteEvent.Reset();
				startReceiveEvent.Set();
				if (!receiveCompleteEvent.WaitOne(receiveTimeoutMilliseconds, false))
				{
					CheckForWorkerThreadUnhandledException();
                    throw new UsbLinkTimeoutException(ERR_9009_MSG);
				}
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Worker thread completed receive");
			}
			Debug.Assert(receivePacketQueue.Count > 0);
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "Dequeuing packet");
			return (UsbLinkPacket)receivePacketQueue.Dequeue();
		}

		//
		// The following methods run from the worker thread.
		//

		private static void ThrowNativeErrorException(int error, string apiName)
		{
            //if (GpsTrace.GpsSwitch.TraceError)
            //    GpsTrace.WriteLine("UsbLinkWorkerThread: ERROR: {0} failed with error code {1}", apiName, error);
			Win32Exception ex = new Win32Exception(error);
            throw new ApplicationException(String.Format(CultureInfo.CurrentCulture, "An unexpected communication error occurred: {0}", ex.Message), ex);
		}

		private void ThreadLoop()
		{
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: ThreadLoop starting");
			try
			{
				WaitHandle[] waitHandles = new WaitHandle[] { startTransmitEvent, startReceiveEvent, pingEvent, exitThreadEvent };
				bool exitThread = false;
				while (!exitThread)
				{
					switch (WaitHandle.WaitAny(waitHandles))
					{
						case 0: // startTransmitEvent
                            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Received startTransmitEvent signal");
							startTransmitEvent.Reset();
							DoTransmit();
							break;
						case 1: // startReceiveEvent
                            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Received startReceiveEvent signal");
							startReceiveEvent.Reset();
							DoReceive();
							break;
						case 2: // pingEvent
                            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Received pingEvent signal");
							pingEvent.Reset();
							DoPing();
							break;
						case 3: // exitThreadEvent
                            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Received exitThreadEvent signal");
							exitThreadEvent.Reset();
							exitThread = true;
							break;
						default:
							Debug.Assert(false);
							break;
					}
				}
			}
			catch (Exception ex)
			{
				workerThreadUnhandledException = ex;
                //if (GpsTrace.GpsSwitch.TraceError)
                //    GpsTrace.WriteLine("UsbLinkWorkerThread: ERROR: Unhandled exception: {0}", workerThreadUnhandledException.Message);
			}
			finally
			{
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: ThreadLoop exiting");
			}
		}

		private void DoTransmit()
		{
			Debug.Assert(transmitPacketQueue.Count == 1);
			UsbLinkPacket packet = (UsbLinkPacket)transmitPacketQueue.Dequeue();
			int packetOffset = 0;
			while (packetOffset < packet.Length)
			{
				int bytesToWrite = Math.Min(packet.Length - packetOffset, MAX_BUFFER_SIZE);
				uint bytesWritten = 0;
                //if (GpsTrace.GpsSwitch.TraceVerbose)
                //    GpsTrace.WriteLine("UsbLinkWorkerThread: Writing data: {0}", new BytePacket(packet.GetBytes(packetOffset, bytesToWrite)));
#if EMULATE_DEVICE
				bytesWritten = (uint)bytesToWrite;
#else
                if (!NativeWin32Methods.WriteFile(deviceHandle, packet.GetBytes(packetOffset, bytesToWrite), (uint)bytesToWrite, out bytesWritten, null))
					ThrowNativeErrorException(Marshal.GetLastWin32Error(), "WriteFile");
#endif
				Debug.Assert((int)bytesWritten == bytesToWrite);
				packetOffset += bytesToWrite;
			}

			if (packet.Length % usbPacketSize == 0)
			{
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Writing data: (empty)");
#if !EMULATE_DEVICE
				uint bytesWritten = 0;
                if (!NativeWin32Methods.WriteFile(deviceHandle, null, 0, out bytesWritten, null))
					ThrowNativeErrorException(Marshal.GetLastWin32Error(), "WriteFile");
#endif
			}

#if EMULATE_DEVICE
			// Set up the emulated responses from the GPS device below.
			// deviceHandle gets incremented with each packet sent to the unit, so the data sent from the unit in response to each
			// transmit packet can be queued at the right time. deviceHandle will be 1 for the first transmit packet, 2 for the second
			// transmit packet, etc. The response data can be taken straight from a GPS trace log file.
			if ((int)deviceHandle == 1)
			{
				interruptInEmulQueue.Enqueue(BytesFromLogData("00-00-00-00-06-00-00-00-04-00-00-00-DD-C8-D6-C4"));
			}
			else if ((int)deviceHandle == 2)
			{
				interruptInEmulQueue.Enqueue(BytesFromLogData("00-00-00-00-02-00-00-00-00-00-00-00"));
				bulkInEmulQueue.Enqueue(BytesFromLogData("14-00-00-00-FF-00-00-00-55-00-00-00-24-01-FA-00-47-50-53-4D-61-70-37-36-43-53-58-20-53-6F-66-74-77-61-72-65-20-56-65-72-73-69-6F-6E-20-32-2E-35-30-00-56-45-52-42-4D-41-50-20-41-6D-65-72-69-63-61-73-20-52-65-63-20-42-61-73-65-6D-61-70-20-34-2E-30-30-00-56-45-52-53-4D-41-50-20-4E-6F-6E-65-00"));
				bulkInEmulQueue.Enqueue(BytesFromLogData("14-00-00-00-F8-00-00-00-46-00-00-00-56-45-52-53-4D-41-50-31-20-41-6D-65-72-69-63-61-73-20-4D-61-72-69-6E-65-20-50-4F-49-20-31-2E-30-30-00-53-49-52-46-47-50-53-20-47-53-43-33-66-20-53-6F-66-74-77-61-72-65-20-56-65-72-73-69-6F-6E-20-32-2E-34-30-00"));
				bulkInEmulQueue.Enqueue(BytesFromLogData("14-00-00-00-FD-00-00-00-78-00-00-00-50-00-00-4C-01-00-41-0A-00-54-01-00-41-64-00-44-6E-00-41-C9-00-44-CA-00-44-6E-00-44-D2-00-41-2D-01-44-38-01-44-2E-01-41-90-01-44-6E-00-41-F4-01-44-F5-01-41-58-02-44-58-02-41-59-02-44-59-02-41-BC-02-44-BC-02-41-20-03-44-20-03-41-84-03-41-86-03-41-87-03-41-88-03-41-8B-03-44-8B-03-44-8C-03-44-8D-03-44-8E-03-41-8C-03-44-8F-03-41-92-03-41-94-03-41-95-03-44-95-03"));
				bulkInEmulQueue.Enqueue(BytesFromLogData(""));
			}
			deviceHandle = (IntPtr)((int)deviceHandle + 1);
#endif
			transmitCompleteEvent.Set();
		}

		private void DoReceive()
		{
			Debug.Assert(receivePacketQueue.Count == 0);
			do
			{
				UsbLinkPacket packet = GetInterruptInPacket();
				if (packet.IsValid)
				{
					if (packet.PacketType == UsbPacketType.UsbProtocolLayer && packet.PacketId == linkPacketIds.DataAvailable)
					{
                        //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Switching to bulk-in");
						ProcessBulkInData(GetBulkInData());
					}
					else
					{
                        //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Enqueuing packet");
						receivePacketQueue.Enqueue(packet);
					}
				}
                //else
                    //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceWarning, "UsbLinkWorkerThread: WARNING: Discarding invalid packet");
			}
			while (receivePacketQueue.Count == 0);
			receiveCompleteEvent.Set();
		}

		private UsbLinkPacket GetInterruptInPacket()
		{
			UsbLinkPacket packet = new UsbLinkPacket();
			uint bytesReturned;
			do
			{
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Calling DeviceIoControl");
#if EMULATE_DEVICE
				while (interruptInEmulQueue.Count == 0);
				byte[] data = (byte[])interruptInEmulQueue.Dequeue();
				Marshal.Copy(data, 0, interruptInBuffer.Pointer, data.Length);
				bytesReturned = (uint)data.Length;
#else
				bytesReturned = 0;
                if (!NativeWin32Methods.DeviceIoControl(deviceHandle, IOCTL_ASYNC_IN, IntPtr.Zero, 0,
					interruptInBuffer.Pointer, (uint)interruptInBuffer.Size, out bytesReturned, null))
					ThrowNativeErrorException(Marshal.GetLastWin32Error(), "DeviceIoControl");
#endif
                //if (GpsTrace.GpsSwitch.TraceVerbose)
                //    GpsTrace.WriteLine("UsbLinkWorkerThread: Received data: {0}", new BytePacket(interruptInBuffer.Pointer, (int)bytesReturned));
				packet.AppendBytes(interruptInBuffer.Pointer, 0, (int)bytesReturned);
			}
			while ((int)bytesReturned == interruptInBuffer.Size);
			return packet;
		}

		private BytePacket GetBulkInData()
		{
			BytePacket packet = new BytePacket();
			uint bytesReturned;
			do
			{
                //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Calling ReadFile");
#if EMULATE_DEVICE
				while (bulkInEmulQueue.Count == 0);
				byte[] data = (byte[])bulkInEmulQueue.Dequeue();
				if (data != null)
				{
					Marshal.Copy(data, 0, bulkInBuffer.Pointer, data.Length);
					bytesReturned = (uint)data.Length;
				}
				else
					bytesReturned = 0;
#else
				bytesReturned = 0;
                if (!NativeWin32Methods.ReadFile(deviceHandle, bulkInBuffer.Pointer, (uint)bulkInBuffer.Size, out bytesReturned, null))
					ThrowNativeErrorException(Marshal.GetLastWin32Error(), "ReadFile");
#endif
                //if (GpsTrace.GpsSwitch.TraceVerbose)
                //    GpsTrace.WriteLine("UsbLinkWorkerThread: Received data: {0}", new BytePacket(bulkInBuffer.Pointer, (int)bytesReturned));
				packet.AppendBytes(bulkInBuffer.Pointer, 0, (int)bytesReturned);
			}
			while ((int)bytesReturned > 0);
			return packet;
		}

		private void ProcessBulkInData(BytePacket bulkInData)
		{
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceVerbose, "UsbLinkWorkerThread: Processing bulk data");
			int offset = 0;
			UsbLinkPacket packet;
			while ((packet = GetNextLinkPacket(bulkInData, ref offset)) != null)
			{
                //if (GpsTrace.GpsSwitch.TraceVerbose)
                //    GpsTrace.WriteLine("UsbLinkWorkerThread: Enqueuing packet={0}", packet);
				receivePacketQueue.Enqueue(packet);
			}
		}

		private UsbLinkPacket GetNextLinkPacket(BytePacket bulkInData, ref int offset)
		{
			int bytesLeft = bulkInData.Length - offset;
			if (bytesLeft == 0)
				return null;
			if (bytesLeft >= UsbLinkPacket.HeaderSize)
			{
				int packetDataSize = bulkInData.GetInt32(offset + 8);
				int packetLength = UsbLinkPacket.HeaderSize + packetDataSize;
				if (bytesLeft >= packetLength)
				{
					UsbLinkPacket packet = new UsbLinkPacket(packetLength);
					packet.AppendBytes(bulkInData.GetBytes(offset, packetLength));
					offset += packetLength;
					if (packet.IsValid)
						return packet;
				}
			}
            //GpsTrace.WriteLineIf(GpsTrace.GpsSwitch.TraceWarning, "UsbLinkWorkerThread: WARNING: Discarding invalid packet and rest of bulk in data");
			return null;
		}

		private void DoPing()
		{
			pingResponseEvent.Set();
		}
	}
}
